// STL
#include <fstream>  // std::ifstream
#include <iostream> // std::cerr
#include <string>   // std::string
#include <tuple>    // std::tuple<>, std::make_tuple()
#include <vector>   // std::vector<>

// Boost
#include <boost/program_options.hpp>   // boost::program_options

// stats++
#include "statsxx/optimization/EA.hpp" // EAParam


//
// DESC: Read the parameters file.
//
std::tuple<
           std::string,         // mlp_file
           // =====
           bool,                // create
           // -----
           bool,                // create_classif
           // -----
           std::vector<int>,    // create_architecture
           int,                 // create_af_type
           std::string,         // create_dbn_file
           // =====
           bool,                // train
           // -----
           int,                 // train_method
           // -----
           int,                 // train_nepoch_min
           int,                 // train_nepoch_max
           // -----
           bool,                // train_early_stopping
           // -----
           double,              // train_lr
           double,              // train_lr_min
           double,              // train_lr_max
           // -----
           double,              // train_momentum
           double,              // train_weight_penalty
           // -----
           double,              // train_qrprop_u
           double,              // train_qrprop_d
           // -----
           double,              // train_scg_lambda
           double,              // train_scg_sigma
           double,              // train_scg_convg_iterfrac
           double,              // train_scg_rk_tol
           // -----
           EAParam,             // EA_param
           // -----
           std::string,         // train_X_in_file
           std::string,         // train_X_out_file
           // -----
           int,                 // train_npts_per_batch
           // =====
           bool,                // test
           // -----
           std::string,         // test_label_file
           std::string,         // test_X_in_file
           std::string,         // test_X_out_file
           // -----
           double,              // test_cutoff
           // =====
           std::string          // prefix
           > read_param_file(
                             const std::string param_filename
                             )
{
    namespace po = boost::program_options;

    //=========================================================

    // ----- PARAMETERS -----
    std::string         mlp_file;            // (always) required
    // =====
    bool                create               = false;
    // -----
    //bool                create_recurrent;
    // -----
    bool                create_classif;      // required for creation, required for testing
    // -----
    std::vector<int>    create_architecture; // required for creation (single setting for DBN initialization, full architecture otherwise)
    int                 create_af_type;      // required for creation
    std::string         create_dbn_file;     // no default
    // =====
    bool                train                = false;
    // -----
    int                 train_method;        // required for training
    // -----
    int                 train_nepoch_min;
    int                 train_nepoch_max;
    // -----
    bool                train_early_stopping;
    // -----
    double              train_lr;
    double              train_lr_min;
    double              train_lr_max;
    // -----
    double              train_momentum;
    double              train_weight_penalty;
    // -----
    double              train_qrprop_u;
    double              train_qrprop_d;
    // -----
    double              train_scg_lambda;
    double              train_scg_sigma;
    double              train_scg_convg_iterfrac;
    double              train_scg_rk_tol;
    // -----
    EAParam             EA_param;
    // -----
    std::string         train_X_in_file;     // required for training
    std::string         train_X_out_file;    // required for training
    // -----
    int                 train_npts_per_batch;
    // =====
    bool                test                 = false;
    // -----
    std::string         test_label_file;     // test_label_file
    std::string         test_X_in_file;      // required for testing
    std::string         test_X_out_file;     // required for testing
    // -----
    double              test_cutoff;
    // =====
    std::string         prefix;

    //=========================================================

    try
    {
        //=========================================================
        // SET THE OPTIONS
        //=========================================================

        po::options_description desc("options");

        desc.add_options()
        ("mlp_file",                 po::value<std::string>(&mlp_file)->required(),                     "MLP filename (input or output)")
        // =====
        //("create.recurrent",         po::value<bool>(&create_recurrent),                                "[create] whether the network is of recurrent type")
        ("create.classif",           po::value<bool>(&create_classif),                                  "[create] whether is classification")
        // -----
        ("create.architecture",      po::value<std::vector<int>>(&create_architecture),                 "[create] architecture")
        ("create.af_type",           po::value<int>(&create_af_type),                                   "[create] type of units")
        ("create.dbn_file",          po::value<std::string>(&create_dbn_file),                          "[create] DBN filename")
        // =====
        ("train.method",             po::value<int>(&train_method),                                     "[train] training method")
        // -----
        ("train.nepoch_min",         po::value<int>(&train_nepoch_min)->default_value(10),              "[train] number of epochs (minimum)")
        ("train.nepoch_max",         po::value<int>(&train_nepoch_max)->default_value(1000),            "[train] number of epochs (maximum)")
        // -----
        ("train.early_stopping",     po::value<bool>(&train_early_stopping)->default_value(true),       "[train] early stopping")
        // -----
        ("train.lr",                 po::value<double>(&train_lr)->default_value(0.01),                 "[train] learning rate (initial)")
        ("train.lr_min",             po::value<double>(&train_lr_min)->default_value(0.0001),           "[train] learning rate (minimum)")
        ("train.lr_max",             po::value<double>(&train_lr_max)->default_value(0.1),              "[train] learning rate (maximum)")
        // -----
        ("train.momentum",           po::value<double>(&train_momentum)->default_value(0.1),            "[train] momentum")
        // -----
        ("train.weight_penalty",     po::value<double>(&train_weight_penalty)->default_value(0.0001),   "[train] weight penalty")
        // -----
        ("train.qrprop.u",           po::value<double>(&train_qrprop_u)->default_value(1.2),            "[train] QRprop u")
        ("train.qrprop.d",           po::value<double>(&train_qrprop_d)->default_value(0.5),            "[train] QRprop d")
        // -----
        ("train.scg.lambda",         po::value<double>(&train_scg_lambda)->default_value(1.e-12),       "[train] SCG lambda")
        ("train.scg.sigma",          po::value<double>(&train_scg_sigma)->default_value(1.e-8),         "[train] SCG sigma")
        ("train.scg.convg_iterfrac", po::value<double>(&train_scg_convg_iterfrac)->default_value(0.25), "[train] SCG convg_iterfrac")
        ("train.scg.rk_tol",         po::value<double>(&train_scg_rk_tol)->default_value(1.e-6),        "[train] SCG rk_tol")
        // -----
        // EA
        // -----
        //
        // NOTE: Defaults for EA_param are set in EAParam.
        //
        // NOTE: The .genome_size is inferred and automatically set.
        //
        ("train.ea.pop_size",                 po::value<int>(&EA_param.pop_size),                       "[train] EA population size")
        // ---
        ("train.ea.max_gen",                  po::value<int>(&EA_param.max_gen),                        "[train] EA max. generations")
        // ---
        ("train.ea.fitness_sharing",          po::value<bool>(&EA_param.fitness_sharing),               "[train] EA fitness sharing")
        ("train.ea.fitness_share_sigma",      po::value<double>(&EA_param.fitness_share_sigma),         "[train] EA fitness sharing sigma")
        ("train.ea.fitness_share_alpha",      po::value<double>(&EA_param.fitness_share_alpha),         "[train] EA fitness sharing alpha")
        // ---
        ("train.ea.frac_elite",               po::value<double>(&EA_param.frac_elite),                  "[train] EA elitism fraction")
        ("train.ea.tournament_nparticipants", po::value<int>(&EA_param.tournament_nparticipants),       "[train] EA number of tournament participants")
        // ---
        ("train.ea.xover_nk",                 po::value<int>(&EA_param.xover_nk),                       "[train] EA crossover number of points (k-point crossover)")
        // ---
        ("train.ea.prob_mutate_only",         po::value<double>(&EA_param.prob_mutate_only),            "[train] EA probability of (only) mutation")
        ("train.ea.prob_mutate",              po::value<double>(&EA_param.prob_mutate),                 "[train] EA probability of mutation")
        ("train.ea.prob_new_gene",            po::value<double>(&EA_param.prob_new_gene),               "[train] EA probability of new gene")
        // ---
        ("train.ea.out_dir",                  po::value<std::string>(&EA_param.out_dir),                "[train] EA output directory")
        // -----
        // -----
        ("train.X_in_file",          po::value<std::string>(&train_X_in_file),                          "[train] X in filename")
        ("train.X_out_file",         po::value<std::string>(&train_X_out_file),                         "[train] X out filename")
        // -----
        ("train.npts_per_batch",     po::value<int>(&train_npts_per_batch)->default_value(-1),          "[train] number of points per batch")
        // =====
        ("test.label_file",          po::value<std::string>(&test_label_file),                          "[test] label filename")
        ("test.X_in_file",           po::value<std::string>(&test_X_in_file),                           "[test] X in filename")
        ("test.X_out_file",          po::value<std::string>(&test_X_out_file),                          "[test] X out filename")
        // -----
        ("test.cutoff",              po::value<double>(&test_cutoff),                                   "[test] cutoff")
        // =====
        ("prefix",                   po::value<std::string>(&prefix)->required(),                       "prefix for output")
        ;

        //=========================================================
        // READ THE FILE
        //=========================================================

        std::ifstream ifs(param_filename, std::ios::in);

        po::variables_map vm;
        po::store(po::parse_config_file(ifs , desc), vm);
        po::notify(vm);

        ifs.close();

        //=========================================================
        // INFER CALCULATION TYPE(S)
        //=========================================================

        // TODO: it would be nice if we could determine create / train / etc. by just checking if a block (e.g., "[train]" was specified)

        // ----- CREATE -----
        if(
           vm.count("create.architecture")
           )
        {
            create = true;
        }

        // ----- TRAIN -----
        if(
           vm.count("train.X_in_file")
           )
        {
            train = true;
        }

        // ----- TEST -----
        if(
           vm.count("test.X_in_file")
           )
        {
            test = true;
        }

/*
        //=========================================================
        // CONSISTENCY / ERROR CHECK
        //=========================================================
        // NOTE: default vectors are also set here

        // TODO: it would be nice to determine interrelations between parameters, rather than all of the messy if checks below
        // TODO: ... but I am not sure that is possible with Boost program_options

        //---------------------------------------------------------
        // CREATE
        //---------------------------------------------------------
        if(create_nunits.size() != create_units_type.size())
        {
            std::cerr << "error in read_param_file(): create_nunits.size() != create_units_type.size()" << '\n';
            //        return false;
            exit(0);
        }

        if(create)
        {
            if(create_nunits.empty())
            {
                std::cerr << "error in read_param_file(): create specified, but create_nunits.empty()" << '\n';
                //        return false;
                exit(0);
            }
        }

        //---------------------------------------------------------
        // TRAIN
        //---------------------------------------------------------
        if(train)
        {
            if(train_X_file.empty())
            {
                std::cerr << "error in read_param_file(): create specified, but train_X_file.empty()" << '\n';
                //        return false;
                exit(0);
            }
        }

        // ----- LEARNING RATE ADJUSTMENTS -----
        if(train_lr_dot.size() != train_lr_adj.size())
        {
            std::cerr << "error in read_param_file(): train_lr_dot.size() != train_lr_adj.size()" << '\n';
            //        return false;
            exit(0);
        }

        if(train_lr_dot.empty())
        {
            train_lr_dot = std::vector<double>(2);
            train_lr_dot[0] = 0.5;
            train_lr_dot[1] = 0.3;
            train_lr_adj = std::vector<double>(2);
            train_lr_adj[0] = 1.05;
            train_lr_adj[1] = 1.01;
        }

        // ----- MOMENTUM ADJUSTMENTS -----
        if(train_p_dot.size() != train_p_adj.size())
        {
            std::cerr << "error in read_param_file(): train_p_dot.size() != train_p_adj.size()" << '\n';
            //        return false;
            exit(0);
        }

        if(train_p_dot.empty())
        {
            train_p_dot = std::vector<double>(1);
            train_p_dot[0] = 0.3;
            train_p_adj = std::vector<double>(1);
            train_p_adj[0] = 1.5;
        }

        //---------------------------------------------------------
        // TEST
        //---------------------------------------------------------
        if(test)
        {
            if(test_X_file.empty())
            {
                std::cerr << "error in read_param_file(): create specified, but test.X_file.empty()" << '\n';
                //        return false;
                exit(0);
            }

            if(test_H_file.empty())
            {
                std::cerr << "error in read_param_file(): create specified, but test.H_file.empty()" << '\n';
                //        return false;
                exit(0);
            }
        }
*/
    }
    catch(std::exception& e)
    {
        std::cerr << "error in read_param_file_0(): " << e.what() << '\n';
//        return false;
        exit(0);
    }
    catch(...)
    {
        std::cerr << "error in read_param_file_0(): unknown" << '\n';
//        return false;
        exit(0);
    }

    //=========================================================
    // FINALIZE AND RETURN
    //=========================================================

    return std::make_tuple(
                           mlp_file,
                           // =====
                           create,
                           // -----
                           create_classif,
                           // -----
                           create_architecture,
                           create_af_type,
                           create_dbn_file,
                           // =====
                           train,
                           // -----
                           train_method,
                           // -----
                           train_nepoch_min,
                           train_nepoch_max,
                           // -----
                           train_early_stopping,
                           // -----
                           train_lr,
                           train_lr_min,
                           train_lr_max,
                           // -----
                           train_momentum,
                           train_weight_penalty,
                           // -----
                           train_qrprop_u,
                           train_qrprop_d,
                           // -----
                           train_scg_lambda,
                           train_scg_sigma,
                           train_scg_convg_iterfrac,
                           train_scg_rk_tol,
                           // -----
                           EA_param,
                           // -----
                           train_X_in_file,
                           train_X_out_file,
                           // -----
                           train_npts_per_batch,
                           // =====
                           test,
                           // -----
                           test_label_file,
                           test_X_in_file,
                           test_X_out_file,
                           // -----
                           test_cutoff,
                           // =====
                           prefix
                           );
}
